Dynamic parameters provide a means by which a host application's user interface (UI) can dynamically create interactive controls for editing parametric values on objects within the model. Selectable objects in the 3D model editor can be extremely diverse and easily customised by end users, so it would be impractical to hard-code UI control panels for each and every different type.
Dynamic parameters allow selectable objects to define for themselves which parametric values to expose, how they should be grouped together and what their display format, title, value type, range, incremental steps and units should be. When the user selects an object in the scene, the UI framework can simply interrogate the list returned by the object's getDynamicParameters(), getTypeComponentParameters() or getJunctionParameters() methods and create UI controls specific to each parameter type.
In addition to a type component, some entities can be assigned additional components which can have their own dynamic parameters. For example, a door can have additional panel and handle components, and a stair can have a railing component, etc. The dynamic parameters for each of these components can be accessed through their own getDynamicParameters() methods, and the host element should automatically manage any changes to their values.
Fundamentals
Dynamic parameters are defined using instances of the PD.Parameter class. Each instance references a PD.ParamType object that determines their value type, format and range, and which can be shared by multiple parameters or customised specific to just a single one. Parameters can then be assembled into control groups by adding them to a PD.ParamGroup instance.
getDynamicParameters()
When an object is selected within the 3D model, the UI may call the getDynamicParameters() method on it. This returns an array of zero or more PD.ParamGroup instances which can then used by the UI to generate corresponding HTML controls.
Any custom element and/or component classes you create will each need to override this method in order to return their own specific parameter groups and parameters, as shown in the two larger examples below.
As described above, and depending on what is currently selected in the UI, the UI may need to call getTypeComponentParameters() or getJunctionParameters() methods to get additional parameters, or the getDynamicParameters() on any of the additional components assigned to the element.
updateDynamicParameters()
If the user changes the value of any of these HTML controls, the UI should call the updateDynamicParameters() method on the object with the parameter that changed and its new value. The default implementation of this method then calls the checkDynamicParameter() method to provide the object an opportunity to do some range limiting or apply more complex logic using related parameters and other properties. It will then compare the new value with its previous value and, if different, will do whatever is required to update the object and the 3D model. Dynamic parameter changes are automatically detected and handled by the PD.GlobalUndo system.
Custom element and component classes will not usually have to override this method as the real work is done by the checkDynamicParameter() method and the individual parameter's setValueOnHostIfDifferent() method.
The default implementation of this method is relatively simple, as shown in the code block below.
updateDynamicParameters(param, group) {
const target = group.getTarget() || this;
target.checkDynamicParameter(param, group, this);
return param.setValueOnHostIfDifferent(target, group, this);
};
However, if you do need to add custom logic or intercept the return value, the you should use either of the two alternate examples shown below as the basis for your own method.
updateDynamicParameters(param, group) {
if (PD.Base.updateDynamicParametersOnHost(param, group, this)) {
if (param.name == 'i_am_special') {
this.doSomethingSpecial();
}
return true;
}
return false;
};
updateDynamicParameters(param, group) {
const target = group.getTarget() || this;
target.checkDynamicParameter(param, group, this);
if (param.setValueOnHostIfDifferent(target, group, this)) {
// If you need access to `target` argument.
if (target.iUseMyOwnMeshThatIsUpdatedDuringRebuild) {
// Rebuild element.
this.hasChanged = true;
this.update();
// If no level meshes or other elements are affected,
// simply update the target locally and return false.
this.iUseMyOwnMeshThatIsUpdatedDuringRebuild.update();
// Update selection meshes if the
// element's highlight geometry changed.
PD.GlobalActions.updateSelectionMeshes();
return false;
}
return true;
}
return false;
};
checkDynamicParameters()
Whilst it is not strictly necessary to do range limiting for each parameter within this method, it is a highly recommended practice as doing so before the parameter is compared to its corresponding property or previous value can prevent triggering unnecessary model rebuilds due to out-of-range parameters being different, even though the geometry of the object won't actually change due to range limiting within the rebuild() method.
If you are using temporal parameters to edit object properties, then you really need to do the same range checking and limiting on both parameter values in the checkDynamicParameter() method, and property values within the rebuild() method, just in case the property has been changed to something invalid somewhere else. It is always important to remember that any user can use the console to change any property on any object to any value at any time they like, so it is good practice to always check and validate input values before you use them.
The following example code shows how you can override this method in your own classes.
checkDynamicParameter(param, group, host) {
switch (param.name) {
// NOTE: Range validation here is not required, but doing so can prevent unnecessary model
// rebuilds due to out-of-range parameters being different from their previous values even
// though the actual geometry won't change due to range checking in the `rebuild()` method.
case 'seatHeight':
param.value = PD.Utils.constrainTo(param.value, 0.0, this.height - this.legSize);
break;
case 'angle':
param.value = PD.Utils.constrainTo(param.value, -180.0, 180.0);
break;
default:
super.checkDynamicParameter(param, group, host);
break;
}
};
Temporal Parameters
In all the core BIM entities, dynamic parameters are temporal and exist only for the duration that they are selected within the 3D model editor. In these cases, an object's dynamic parameters and their groupings are created each time its getDynamicParameters() method is called. The name of each parameter is the same as that of a property that exists on the object, and any changes to the parameter automatically update that property value. This way, dynamic parameters are created and freed on each selection and deselection and only exist whilst they are actually needed.
This temporal approach is great for maintaining complete separation between a reactive UI and the objects within the 3D model. The UI requests and stores the parameters and groups from the object but, once they are provided, the model object does not need to know anything about them and they will be automatically disposed of when the UI is no longer using them. Also, the corresponding object properties are only changed within that object's updateDynamicParameters() method, so there is no possibility of any reactivity or observability 'leaking' into the 3D model from a particularly enthusiastic UI framework.
Whilst this is recommended approach, one of its downsides is the need to manually maintain the relationships between properties and parameters - making sure that the names properly match, that changes to one are always reflected in the other, ensuring that property values are within the same ranges as the parameters, etc. It also means having to add each property to the toJSON() and fromJSON() methods of the host object and check their values, etc. This may sound trivial, but the parameters exist only in the getDynamicParameters() method whilst the properties are referenced in several different methods and require additional boilerplate code to keep everything in sync.
The example code below shows an example custom component that adds uses two temporal dynamic parameters to edit two properties.
const MyComponent = class extends BIM.Component {
constructor(config, typeName) {
config = config || {};
super(config, typeName || MyComponent.getDisplayName());
this.seatHeight = Math.max(0.0, PD.Utils.toDimension(config.seatHeight, 16, 400)); // 16" or 400mm.
this.angle = PD.Utils.toNumberInRange(config.angle, 0.0, -180.0, 180.0);
};
// ...
toJSON(data) {
data = super.toJSON(data);
data.seatHeight = this.seatHeight;
data.angle = this.angle;
return data;
};
// ...
fromJSON(data) {
super.fromJSON(data);
if ('seatHeight' in data) {
this.seatHeight = Math.max(0.0, PD.Utils.toNumber(data.seatHeight, this.seatHeight));
}
if ('angle' in data) {
this.angle = PD.Utils.toNumberInRange(data.angle, this.angle, -180.0, 180.0);
}
return this;
};
// ...
getDynamicParameters(host) {
return [
new PD.ParamGroup({
name: 'chairParams',
title: 'Basic Chair Parameters',
target: this,
params: [
new PD.Parameter({
name: 'seatHeight',
title: 'Seat Height',
description: 'The height from floor level to the top of the seating surface.',
paramType: PD.ParamType.SmallDistance,
value: this.seatHeight,
}),
new PD.Parameter({
name: 'angle',
title: 'Rotation Angle',
description: 'The angle of rotation around the Z axis.',
paramType: PD.ParamType.getCoreType('Azimuth'),
value: this.angle
})
]
})
];
};
// ...
checkDynamicParameter(param, group, host) {
switch (param.name) {
case 'seatHeight':
param.value = PD.Utils.constrainTo(param.value, 0.0, this.height - this.legSize);
break;
case 'angle':
param.value = PD.Utils.constrainTo(param.value, -180.0, 180.0);
break;
default:
super.checkDynamicParameter(param, group, host);
break;
}
};
// ...
rebuild(element) {
const path = element.path;
const shell = element.shell;
// Check and validate properties as extra protection in case they were changed elsewhere.
const seat_height = PD.Utils.constrainTo(this.seatHeight, 0.0, this.height - this.legHeight);
const angle = PD.Utils.constrainTo(this.angle, -180.0, 180.0);
// ... generate geometry ...
}
// ...
};
As you can see, the added property names are referenced in six (6) separate methods in the custom class, which is a fair bit of boilerplate code to maintain and keep in sync. This is appropriate for the core classes as using temporal parameters is more memory efficient when there are large numbers of core elements in a model. However, for custom elements where there may only be a small number of instances within a model, using persistent parameters may be more appropriate for you.
Persistent Parameters
Another option is to create the dynamic parameters you need within the constructor of your custom class and store them in an object literal such as this.parameters. You can also create your parameter groups in an array such as this.paramGroups. If you do this, access to their value must be via this.parameters.seatHeight.value rather than this.seatHeight, and both the parameters and parameter groups will persist for the life of the entity within the model.
You will also have to override your custom class's toJSON() and fromJSON() methods to use the static PD.Base.getParametersAsDataObject and PD.Base.updateParametersFromDataObject methods.
The example code below shows how you would set up and use persistent dynamic parameters.
const MyComponent = class extends BIM.Component {
constructor(config, typeName) {
config = config || {};
super(config, typeName || MyComponent.getDisplayName());
this.parameters = {
seatHeight: new PD.Parameter({
name: 'seatHeight', // Must match corresponding property name.
title: 'Seat Height',
description: 'The height from floor level to the top of the seating surface.',
value: Math.max(0.0, PD.Utils.toDimension(config.seatHeight, 16, 400)), // 16" or 400mm.
paramType: PD.ParamType.SmallDistance
}),
angle: new PD.Parameter({
name: 'angle', // Must match corresponding property name.
title: 'Rotation Angle',
description: 'The angle of rotation around the Z-axis.',
value: PD.Utils.toNumber(config.angle, 0.0),
paramType: PD.ParamType.getCoreType('Azimuth')
})
};
this.paramGroups = [
new PD.ParamGroup({
name: 'chairParams',
title: 'Basic Chair Parameters',
target: this.parameters,
params: [
this.parameters.seatHeight,
this.parameters.angle
]
})
];
};
// ...
toJSON(data) {
data = super.toJSON(data);
// Add parameter values.
data.parameters = PD.Base.getParametersAsDataObject(this.parameters);
return data;
};
// ...
fromJSON(data) {
super.fromJSON(data);
// Get parameter values.
if ('parameters' in data) {
PD.Base.updateParametersFromDataObject(data.parameters, this.parameters);
}
return this;
};
// ...
getDynamicParameters(host) {
return this.paramGroups;
};
// ...
rebuild(element) {
const path = element.path;
const shell = element.shell;
// Check and validate params as extra protection.
const seat_height = PD.Utils.constrainTo(this.parameters.seatHeight.value, 0.0, this.height - this.legSize);
const angle = PD.Utils.constrainTo(this.parameters.angle.value, -180.0, 180.0);
// ... generate geometry ...
}
// ...
};
Updating Dynamic Parameters
As one of the aims of dynamic parameters is to maintain a complete separation between a reactive UI and the objects within the 3D model, data flow is intentionally one directional - from parameter to property. This can make updating currently displayed dynamic parameters based on changes to the model a little circuitous. To assist with this, the PD.ParamGroup class provides some methods for finding and updating other parameters.
For example, you can use these methods to modify parameters created by the parent class of your custom class as shown below.
const MyComponent = class extends BIM.Component {
// ...
getDynamicParameters() {
// Get parameters from base class.
const groups = super.getDynamicParameters();
// Modify first group.
const group = groups[0];
if (group) {
group.name = 'my_comp_Params';
group.title = 'My Component Parameters';
group.setParameterProperties('height', {
description: 'The total height of my component',
title: 'Component Height'
});
group.setParameterProperty('size',
'title', 'Component Size'
);
}
return groups;
};
// ...
};
Alternatively, if you have some interdependent parameters, you can add logic to your custom checkDynamicParameter() method to change the dependant properties or to update their values. The following example shows how to enable/disable one parameter based on the value of another, or set the value of one based on another.
const MyComponent = class extends BIM.Component {
// ...
checkDynamicParameter(param, group, host) {
switch (param.name) {
// ...
case 'hasTopRail':
group.setParameterProperty('topRailSize', 'enabled', param.value);
break;
case 'hasBotRail':
group.setParameterProperty('botRailSize', 'enabled', param.value);
break;
// -----
case 'topTaper':
param.value = PD.Utils.toNumberInRange(param.value, this.topTaper, 0.0, 100.0);
if (this.mirrorTaper) {
this.botTaper = param.value;
group.updateParameter('botTaper');
}
break;
case 'botTaper':
param.value = PD.Utils.toNumberInRange(param.value, this.botTaper, 0.0, 100.0);
if (this.mirrorTaper) {
this.topTaper = param.value;
group.updateParameter('topTaper');
}
break;
case 'mirrorTaper':
if (param.value) {
this.topTaper = this.botTaper = Math.min(this.botTaper, this.topTaper);
group.updateParameter('topTaper');
group.updateParameter('botTaper');
}
break;
// ...
};
};
// ...
};